package com.alogic.load;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import com.anysoft.util.resource.ResourceFactory;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.anysoft.util.Configurable;
import com.anysoft.util.Factory;
import com.anysoft.util.IOTools;
import com.anysoft.util.JsonTools;
import com.anysoft.util.Properties;
import com.anysoft.util.PropertiesConstants;
import com.anysoft.util.Reportable;
import com.anysoft.util.Settings;
import com.anysoft.util.XMLConfigurable;
import com.anysoft.util.XmlElementProperties;
import com.anysoft.util.XmlTools;

/**
 * 扫描器，用于列表性数据的加载
 * 
 * @author duanyy
 * @since 1.6.12.30 [20190425 duanyy] <br>
 *
 * @version 1.6.13.7 [20200720 duanyy] <br>
 * - 修复文件句柄泄漏的bug <br>
 *
 * @version 1.6.13.31 [20210108 duanyy] <br>
 * - 加载sink对象列表时增加enable开关 <br>
 *
 * @version 1.6.14.1 [20210310 duanyy] <br>
 * - 增加XmlResource和OutterXmlResource实现 <br>
 *
 * @version 1.6.14.10 [20210510 duanyy] <br>
 * - 修复Scanner的bug; <br>
 */
public interface Scanner<O extends Loadable> extends Configurable,XMLConfigurable,Reportable{
	
	/**
	 * 获取id
	 * @return id
	 */
	public String getId();
	
	/**
	 * 开始扫描
	 * @param listener
	 */
	public void scan(Listener<O> listener);
	
	/**
	 * 虚基类
	 * 
	 * @author duanyy
	 *
	 * @param <O> 数据对象
	 */
	public abstract static class Abstract<O extends Loadable> implements Scanner<O>{
		/**
		 * a logger of log4j
		 */
		protected static final Logger LOG = LoggerFactory.getLogger(Loader.class);
		protected String id;
		
		@Override
		public void configure(Element e, Properties p) {
			Properties props = new XmlElementProperties(e,p);
			configure(props);
		}
		
		@Override
		public void configure(Properties p) {
			id = PropertiesConstants.getString(p,"id",this.toString(),true);
		}

		@Override
		public String getId() {
			return id;
		}		

		@Override
		public void report(Element xml) {
			if (xml != null){
				XmlTools.setString(xml,"module",getClass().getName());
			}
		}

		@Override
		public void report(Map<String, Object> json) {
			if (json != null){
				JsonTools.setString(json,"module",getClass().getName());
			}
		}		
	}
	
	/**
	 * Sinkable
	 * @author duanyy
	 *
	 * @param <O> 数据对象
	 */
	public static class Sinkable<O extends Loadable> extends Abstract<O> {
		
		/**
		 * 子scanner
		 */
		protected List<Scanner<O>> scanners = new ArrayList<Scanner<O>>();
		
		@Override
		public void configure(Element e, Properties p) {
			Properties props = new XmlElementProperties(e,p);
			configure(props);
			
			NodeList nodeList = XmlTools.getNodeListByPath(e, getSinkTag());
			Factory<Scanner<O>> factory = new Factory<Scanner<O>>();
			String scope = PropertiesConstants.getString(p, "ketty.scope", "runtime");
			
			for (int i = 0 ;i < nodeList.getLength() ; i ++){
				Node n = nodeList.item(i);
				
				if (Node.ELEMENT_NODE != n.getNodeType()){
					continue;
				}
				
				Element elem = (Element)n;

				XmlElementProperties itemProps = new XmlElementProperties(elem,props);
				String itemScope = PropertiesConstants.getString(itemProps,"scope","",true);
				if (StringUtils.isNotEmpty(itemScope) && !itemScope.equals(scope)){
					continue;
				}

				boolean enable = PropertiesConstants.getBoolean(itemProps,"enable",true,true);
				if (!enable){
					continue;
				}
				
				try {
					Scanner<O> loader = factory.newInstance(elem, props, "module");
					if (loader != null){
						scanners.add(loader);
					}
				}catch (Exception ex){
					LOG.error("Can not create loader from element:" + XmlTools.node2String(elem));
					LOG.error(ExceptionUtils.getStackTrace(ex));
				}
			}
		}
		
		/**
		 * 获取Sink的tag名
		 * @return tag名
		 */
		protected String getSinkTag(){
			return "sink";
		}

		@Override
		public void scan(Listener<O> listener) {
			if (listener != null){
				Object cookie = listener.begin(this.getId());
				
				scanSelf(cookie,listener);				
				scanSinks(cookie,listener);
				
				listener.end(cookie, this.getId());
			}
		}
		
		protected void scanSelf(Object cookie,Listener<O> listener){
			
		}
		
		protected void scanSinks(Object cookie,Listener<O> listener){
			for (Scanner<O> scanner:scanners){
				try {
					scanner.scan(listener);
				}catch (Exception ex){
					LOG.error(String.format("Failed to scan scanner:%s",scanner.getId()));
					LOG.error(ExceptionUtils.getStackTrace(ex));
				}
			}			
		}
	}
	
	/**
	 * Cached
	 * @author duanyy
	 *
	 * @param <O> 数据对象
	 */
	public static class Cached<O extends Loadable> extends Sinkable<O> implements Listener<O>{
		
		/**
		 * 子scanner
		 */
		protected List<Scanner<O>> scanners = new ArrayList<Scanner<O>>();
		protected List<O> cached = new ArrayList<O>();
		
		protected long ttl = 60 * 10000;
		protected long lastTimestamp = 0;
		
		@Override
		public void configure(Properties p) {
			super.configure(p);
			
			ttl = (PropertiesConstants.getLong(p, "ttl", 60))*1000;
		}
		
		protected boolean isExpired(long now){
			return now - this.lastTimestamp > ttl;
		}
		
		@Override
		protected void scanSelf(Object cookie,Listener<O> listener){
			long now = System.currentTimeMillis();
			if (isExpired(now)){
				//缓存数据过期，重新加载
				synchronized(this){
					Object scanCookies = begin(this.getId());
					scanSinks(scanCookies,this);
					end(scanCookies,this.getId());
				}
			}
			for (O data:cached){
				listener.found(cookie, data);
			}
		}
		
		@Override
		public void scan(Listener<O> listener) {
			if (listener != null){
				Object cookie = listener.begin(this.getId());
				
				scanSelf(cookie,listener);	
				
				listener.end(cookie, this.getId());
			}
		}

		@Override
		public Object begin(String scannerId) {
			if (scannerId != null && scannerId.equals(this.getId())) {
				cached.clear();
			}
			return this;
		}

		@Override
		public void found(Object cookie, O data) {
			cached.add(data);
		}

		@Override
		public void end(Object cookie, String scannerId) {
			this.lastTimestamp = System.currentTimeMillis();
		}
	}

	/**
	 * 基于XML节点扫描
	 * @param <O>
	 */
	public abstract static class XmlResource<O extends Loadable> extends Abstract<O> {
		protected abstract String getObjectXmlTag();
		protected abstract String getObjectDftClass();

		protected List<O> objectList = new ArrayList<O>();

		public List<O> getList(){
			return objectList;
		}

		@Override
		public void configure(Element root, Properties p) {
			super.configure(root, p);

			Properties props = new XmlElementProperties(root,p);
			loadFromElement(root,props);
		}

		protected void loadFromElement(Element root,Properties p){
			NodeList nodeList = XmlTools.getNodeListByPath(root, getObjectXmlTag());
			Factory<O> factory = new Factory<O>();

			for (int i = 0 ;i < nodeList.getLength() ; i ++){
				Node n = nodeList.item(i);

				if (Node.ELEMENT_NODE != n.getNodeType()){
					continue;
				}

				Element e = (Element)n;
				try {
					O instance = factory.newInstance(e, p, "module", getObjectDftClass());
					if (StringUtils.isNotEmpty(instance.getId())) {
						add(instance.getId(), instance);
					}
				}catch (Exception ex){
					LOG.warn("Failed to create object:" + XmlTools.node2String(e));
					LOG.warn(ExceptionUtils.getStackTrace(ex));
				}
			}
		}

		protected void add(String id, O instance){
			objectList.add(instance);
		}

		@Override
		public void scan(Listener<O> listener) {
			if (listener != null){
				Object cookie = listener.begin(this.getId());

				for (O data:objectList){
					listener.found(cookie, data);
				}

				listener.end(cookie, this.getId());
			}
		}
	}

	/**
	 * 基于外部XML文件扫描
	 * @param <O>
	 */
	public abstract static class OutterXmlResource<O extends Loadable> extends Abstract<O> {
		protected abstract String getObjectXmlTag();
		protected abstract String getObjectDftClass();

		protected List<O> objectList = new ArrayList<O>();

		public List<O> getList(){
			return objectList;
		}

		@Override
		public void configure(Properties p) {
			super.configure(p);

			String path = PropertiesConstants.getString(p, "path", "");
			if (StringUtils.isNotEmpty(path)){
				loadFromPath(path,p);
			}
		}

		protected void loadFromPath(String path, Properties p) {
			ResourceFactory rf = Settings.getResourceFactory();
			InputStream in = null;
			try {
				in = rf.load(path, null, null);
				Document doc = XmlTools.loadFromInputStream(in);
				if (doc != null){
					loadFromElement(doc.getDocumentElement(),p);
				}
			} catch (Exception ex) {
				LOG.error("Error occurs when load xml file,source=" + path);
				LOG.error(ExceptionUtils.getStackTrace(ex));
			} finally {
				IOTools.closeStream(in);
			}
		}

		protected void loadFromElement(Element root,Properties p){
			NodeList nodeList = XmlTools.getNodeListByPath(root, getObjectXmlTag());
			Factory<O> factory = new Factory<O>();

			for (int i = 0 ;i < nodeList.getLength() ; i ++){
				Node n = nodeList.item(i);

				if (Node.ELEMENT_NODE != n.getNodeType()){
					continue;
				}

				Element e = (Element)n;
				try {
					O instance = factory.newInstance(e, p, "module", getObjectDftClass());
					if (StringUtils.isNotEmpty(instance.getId())) {
						add(instance.getId(), instance);
					}
				}catch (Exception ex){
					LOG.warn("Failed to create object:" + XmlTools.node2String(e));
					LOG.warn(ExceptionUtils.getStackTrace(ex));
				}
			}
		}

		protected void add(String id, O instance){
			objectList.add(instance);
		}

		@Override
		public void scan(Listener<O> listener) {
			if (listener != null){
				Object cookie = listener.begin(this.getId());

				for (O data:objectList){
					listener.found(cookie, data);
				}

				listener.end(cookie, this.getId());
			}
		}
	}

	/**
	 * 热部署文件
	 * 
	 * @author yyduan
	 *
	 */
	public static abstract class HotFile<O extends Loadable> extends Abstract<O> implements Runnable{
		protected String digest = "";
		protected String filePath;
		protected ScheduledThreadPoolExecutor threadPool = new ScheduledThreadPoolExecutor(1);
		protected long interval = 10;
		protected long delay = 10;
		
		protected List<O> cached = new ArrayList<O>();
		
		@Override
		public void configure(Properties p){
			super.configure(p);		
			filePath = PropertiesConstants.getString(p, "path", "");
			interval = PropertiesConstants.getLong(p,"interval",interval);
			delay = PropertiesConstants.getLong(p,"delay",delay);
			
			loadFromPath(cached,filePath);
			
			threadPool.scheduleAtFixedRate(this, delay, interval, TimeUnit.SECONDS);
		}

		@Override
		public void scan(Listener<O> listener) {
			if (listener != null){
				Object cookie = listener.begin(this.getId());

				for (O data:cached){
					listener.found(cookie, data);
				}

				listener.end(cookie, this.getId());
			}
		}

		/**
		 * 从指定路径装入信息
		 * 
		 * @param path
		 */
		protected void loadFromPath(final List<O> container,final String path){
			File file = new File(path);
			if (file.exists() && file.isFile()){
				digest = getFileDigest(file);
				loadFromFile(container,file);
			}else{
				LOG.warn("File does not exist :" + path);
			}
		}
		
		protected synchronized void loadFromFile(final List<O> container,final File file){
			InputStream in = null;			
			try {
				in = new FileInputStream(file);
				Document doc = XmlTools.loadFromInputStream(in);
				if (doc != null){
					loadFromElement(container,doc.getDocumentElement(),Settings.get());
				}
			} catch (Exception e) {
				LOG.error("Can not open file : " + file.getPath());
				LOG.error(ExceptionUtils.getStackTrace(e));
			}  finally{
				IOTools.close(in);
			}		
		}

		protected void loadFromElement(final List<O> container,Element root,Properties p){
			NodeList nodeList = XmlTools.getNodeListByPath(root, getObjectXmlTag());
			Factory<O> factory = new Factory<O>();
			
			for (int i = 0 ;i < nodeList.getLength() ; i ++){
				Node n = nodeList.item(i);
				
				if (Node.ELEMENT_NODE != n.getNodeType()){
					continue;
				}
				
				Element e = (Element)n;
				O instance = factory.newInstance(e, p, "module", getObjectDftClass());
				objectFound(container,instance);
			}
		}

		protected void objectFound(final List<O> container,O instance){
			if (StringUtils.isNotEmpty(instance.getId())){
				container.add(instance);
			}
		}
		
		protected String getObjectXmlTag() {
			return "model";
		}

		protected abstract String getObjectDftClass();
		
		@Override
		public void run() {
			File file = new File(filePath);
			if (file.exists() && file.isFile()){
				String md5 = getFileDigest(file);
				if (md5 != null && ! md5.equals(digest)){
					digest = md5;				
					LOG.info("File has been changed:" + filePath);
					List<O> temp = new ArrayList<O>();
					loadFromFile(temp,file);				
					cached = temp;
				}
			}
		}
		
		protected synchronized String getFileDigest(File file){
			String digest = null;
			InputStream in = null;
			try {
				in = new FileInputStream(file);
				digest = DigestUtils.md5Hex(in);				
			} catch (Exception e) {
				LOG.error("Can not open file : " + file.getPath());
				LOG.error(ExceptionUtils.getStackTrace(e));
			}finally{
				IOTools.close(in);
			}
			return digest;
		}

	}	
	
	/**
	 * 扫描监听器
	 * @author duanyy
	 *
	 * @param <O> 数据对象
	 */
	public static interface Listener<O extends Loadable> {
		public Object begin(String scannerId);
		public void found(Object cookie,O data);
		public void end(Object cookie,String scannerId);
	}
}
